TypeScript+webpack+AWS CDKで開発環境を構築してLambdaをデプロイしてみた
はじめに
CX事業本部@東京の佐藤智樹です。
今回はTypeScriptでLambdaのコードを記述してwebpackでトランスパイル・バンドルする手順を紹介します。LambdaのデプロイにはAWS CDKを使用します。普段使っている方法で別記事で引用しようと思ったのですが見当たらなかったので個別の記事にしました。
この記事を読めばAWS CDKとTypeScriptで継続して開発する際に便利な環境が構築できるようになります。今回の記事で作成した内容を土台としてTypeScriptでサーバーレスアプリーケーションなどの構築にも活用できます。普段使用している設定を入れているので慣れたら適宜好みに合わせて変更してください。
実行環境
項目 | バージョン |
---|---|
OS | Mac OS Catalina |
AWS CDK | 1.46.0 |
TypeScript | 3.7.2 |
yarn | 1.21.1 |
webpack | 4.43.0 |
webpackを使う利点
TypeScriptは現状そのままだとLambdaで実行できないのでJavaScriptへトランスパイルする必要があります。単なるコードの変換だけでなく依存関係のあるライブラリもバンドルしてデプロイする必要があります。
Lambda Layerにnode_modulesを追加する方法などもありますが、node_modulesに入っている余分なファイルもアップロードされます。webpackであればバンドルする際に、実行で必要なライブラリだけ選別してまとめることができるなどバンドル設定を細かく指定ができたり色々利点があります。
webpackの詳細については公式ドキュメントか以下の記事が詳細に書かれているのでおすすめです。自分も公式ドキュメントと以下の記事をみてパラメータなどを理解しました。
実行環境の整備
本章では実行環境に必要なコマンドラインツールなどをインストールします。
AWS CDK、yarnのインストール
まずnpmを使用してCDKのCLIをインストールします。npmをインストールしていない場合は、brewなどを使用してインストールしてください。
$ npm install -g aws-cdk
次に今回はnpmの代わりにyarnでパッケージを管理するので、yarnをインストールしてください。
$ brew install yarn
CDKでプロジェクトを作成
以下のコマンドでcdkのプロジェクトを作成できます。
$ cdk init --language typescript
またnpmはこの後使用しないのでnpm用のファイルは以下のコマンドで削除します。
$ rm package-lock.json
yarnでwebpackとトランスパイルに関連するライブラリを追加
以下のコマンドでyarnを使用して依存関係のあるライブラリをインポート後、webpackに必要な設定とTypeScriptをJavaScriptにトランスパイルするためのts-loaderを追加します。デプロイ時には不要な設定なので、yarn addの際には-D
を付けてデプロイ時には含まれないようにします。
yarn install yarn add -D webpack webpack-cli webpack-node-externals ts-loader
webpackとTypeScriptを設定
webpack.config.jsを使用してwebpackのバンドル設定を記述します。
本来であればwebpack init
コマンドでconfigファイルを生成したかったのですが、うまく動かなかったので今回はtouch
で空ファイルを作成します。
$ touch webpack.config.js
作成したファイルを以下のように編集します。コメント欄は簡単な解説のため記載しているので適宜削除してください。コメントで記述していない内容は最初に紹介したwebpackの記事でほとんど紹介されています。
webpackのentry内で指定している./src/lambda/handlers/handler.ts
のファイルを元に依存関係などがバンドルされてdistフォルダ配下にJavaScriptファイルが作成されます。
const path = require('path'); const nodeExternals = require('webpack-node-externals'); module.exports = { mode: 'development', target: 'node', entry: { handler: path.resolve( __dirname, './src/lambda/handlers/handler.ts', ), }, // 依存ライブラリをデプロイ対象とするか設定(対象はpackage.json参照) // devDependencies:開発時に必要なライブラリを入れる // dependencies:実行時に必要なライブラリを入れる externals: [ nodeExternals({ modulesFromFile: { exclude: ['dependencies'], include: ['devDependencies'], }, }), ], output: { filename: '[name]/index.js', path: path.resolve(__dirname, 'dist'), libraryTarget: 'commonjs2', }, // 変換後ソースと変換前ソースの関連付け devtool: 'inline-source-map', module: { rules: [ { // ローダーが処理対象とするファイルを設定 test: /\.ts$/, // 先ほど追加したts-loaderを設定 use: [ { loader: 'ts-loader', }, ], }, ], }, // import時のファイル指定で拡張子を外す // https://webpack.js.org/configuration/module/#ruleresolve resolve: { extensions: ['.ts', '.js'], }, };
次にTypeScriptの設定を変更します。変更する内容は型定義ファイルをtsファイルと同じ場所に出力させないだけなので飛ばしても問題ないです。
{ "compilerOptions": { "target": "ES2018", "module": "commonjs", "lib": ["es2018"], "declaration": false, ←ここだけfalseに変更 "strict": true, "noImplicitAny": true, "strictNullChecks": true, "noImplicitThis": true, "alwaysStrict": true, "noUnusedLocals": false, "noUnusedParameters": false, "noImplicitReturns": true, "noFallthroughCasesInSwitch": false, "inlineSourceMap": true, "inlineSources": true, "experimentalDecorators": true, "strictPropertyInitialization": false, "typeRoots": ["./node_modules/@types"] }, "exclude": ["cdk.out"] }
最後にデプロイなどのコマンドを簡略化するためscript配下の内容を変更します。build
をwebpack
、deploy
をcdk deploy
に設定します。項目の記載がない場合追加してください。
{ "name": "lambda-unit", "version": "0.1.0", "bin": { "lambda-unit": "bin/lambda-unit.js" }, "scripts": { "build": "webpack", "deploy": "cdk deploy", "watch": "tsc -w", "test": "jest", "cdk": "cdk" }, "devDependencies": { "@aws-cdk/assert": "1.46.0", "@types/jest": "^25.2.1", "@types/node": "10.17.5", "@webpack-cli/info": "^0.2.0", "aws-cdk": "1.45.0", "jest": "^25.5.0", "ts-jest": "^25.3.1", "ts-loader": "^7.0.5", "ts-node": "^8.1.0", "typescript": "~3.7.2", "webpack": "^4.43.0", "webpack-cli": "^3.3.12", "webpack-node-externals": "^1.7.2" }, "dependencies": { "@aws-cdk/core": "1.46.0", "source-map-support": "^0.5.16" } }
Lambdaを実装
Lambdaの実装を追加します。webpackの設定で指定した./src/lambda/handlers/handler.ts
を作成します。まずはデプロイするためのディレクトリを作成します。
$ mkdir -p src/lambda/handlers
次にLambdaのソースを作成します。内容はデプロイのテストだけできれば良いので簡易なものにしています。
export interface TestEvent { id: number; eventValue?: string; } export async function handler(event: TestEvent): Promise<TestEvent | void> { console.log(JSON.stringify(event)); return event; }
CDKの設定
デプロイするためにCDKのライブラリ追加と設定変更を行います。まずはLambdaを使用するためにyarnでcdkのライブラリを追加します。
$ yarn add "@aws-cdk/aws-lambda"
次にLambdaをデプロイするための設定をCDKのスタックファイルに記述します。今回はシンプルにLambdaの追加のみを行います。スタック名はlambda-unit
というフォルダでプロジェクトを作成したのでそれが反映されています。
import * as cdk from '@aws-cdk/core'; import * as lambda from '@aws-cdk/aws-lambda'; export class LambdaUnitStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); new lambda.Function( this, 'testFunction', { // webpackでバンドルしたファイルを設定 code: lambda.Code.fromAsset(`dist/handler`), functionName: `test-handler`, handler: 'index.handler', runtime: lambda.Runtime.NODEJS_12_X, timeout: cdk.Duration.seconds(10), }, ); } }
以上で事前の設定は完了です。
webpackの実行とAWS環境へのデプロイ
まず以下のコマンドでwebpackを実行してソースをバンドルします。
$ yarn build
するとdist
フォルダが自動で作成され、dist/handler
フォルダの中にトランスパイルされたindex.js
ファイルが作成されます。
(中略) /***/ "./src/lambda/handlers/handler.ts": /*!****************************************!*\ !*** ./src/lambda/handlers/handler.ts ***! \****************************************/ /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); async function handler(event) { console.log(JSON.stringify(event)); return event; } exports.handler = handler; (中略)
次にCDKでデプロイを行っていきます。まず最初にaws-cliを使用できる状態にしてからS3バケットの作成などを以下のコマンドで実行します。
$ cdk bootstrap ⏳ Bootstrapping environment aws://XXXXXXXXXXXX/ap-northeast-1... CDKToolkit: creating CloudFormation changeset... 0/3 | 0:00:35 | CREATE_IN_PROGRESS | AWS::S3::Bucket | StagingBucket 0/3 | 0:00:38 | CREATE_IN_PROGRESS | AWS::S3::Bucket | StagingBucket Resource creation Initiated 1/3 | 0:01:00 | CREATE_COMPLETE | AWS::S3::Bucket | StagingBucket 1/3 | 0:01:03 | CREATE_IN_PROGRESS | AWS::S3::BucketPolicy | StagingBucketPolicy 1/3 | 0:01:05 | CREATE_IN_PROGRESS | AWS::S3::BucketPolicy | StagingBucketPolicy Resource creation Initiated 2/3 | 0:01:05 | CREATE_COMPLETE | AWS::S3::BucketPolicy | StagingBucketPolicy 3/3 | 0:01:06 | CREATE_COMPLETE | AWS::CloudFormation::Stack | CDKToolkit ✅ Environment aws://XXXXXXXXXXXX/ap-northeast-1 bootstrapped. ✨ Done in 46.59s.
次にいよいよ以下のコマンドでデプロイを行います。一度IAMリソースの更新で確認がでるのでy
を押して、問題なければ以下のようにデプロイが完了します。
$ yarn deploy ~ IAM Statement Changes ┌───┬─────────────────────────────────┬────────┬────────────────┬──────────────────────────────┬───────────┐ │ │ Resource │ Effect │ Action │ Principal │ Condition │ ├───┼─────────────────────────────────┼────────┼────────────────┼──────────────────────────────┼───────────┤ │ + │ ${testFunction/ServiceRole.Arn} │ Allow │ sts:AssumeRole │ Service:lambda.amazonaws.com │ │ └───┴─────────────────────────────────┴────────┴────────────────┴──────────────────────────────┴───────────┘ IAM Policy Changes ┌───┬─────────────────────────────┬──────────────────────────────────────────────────────────────────────────┐ │ │ Resource │ Managed Policy ARN │ ├───┼─────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤ │ + │ ${testFunction/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecuti │ │ │ │ onRole │ └───┴─────────────────────────────┴──────────────────────────────────────────────────────────────────────────┘ ~ Do you wish to deploy these changes (y/n)? y LambdaUnitStack: deploying... [0%] start: Publishing 93ee72a91afad5a3251db04b4367b5c7195b59350b289b13e96fd78ed999999:current [100%] success: Published 93ee72a91afad5a3251db04b4367b5c7195b59350b289b13e96fd78ed1999999:current LambdaUnitStack: creating CloudFormation changeset... 0/4 | 0:02:09 | CREATE_IN_PROGRESS | AWS::IAM::Role | testFunction/ServiceRole (testFunctionServiceRoleFEC29B6F) 0/4 | 0:02:09 | CREATE_IN_PROGRESS | AWS::CDK::Metadata | CDKMetadata 0/4 | 0:02:10 | CREATE_IN_PROGRESS | AWS::IAM::Role | testFunction/ServiceRole (testFunctionServiceRoleFEC29B6F) Resource creation Initiated 0/4 | 0:02:11 | CREATE_IN_PROGRESS | AWS::CDK::Metadata | CDKMetadata Resource creation Initiated 1/4 | 0:02:11 | CREATE_COMPLETE | AWS::CDK::Metadata | CDKMetadata 2/4 | 0:02:27 | CREATE_COMPLETE | AWS::IAM::Role | testFunction/ServiceRole (testFunctionServiceRoleFEC29B6F) 2/4 | 0:02:30 | CREATE_IN_PROGRESS | AWS::Lambda::Function | testFunction (testFunction483F4CBA) 2/4 | 0:02:31 | CREATE_IN_PROGRESS | AWS::Lambda::Function | testFunction (testFunction483F4CBA) Resource creation Initiated 3/4 | 0:02:32 | CREATE_COMPLETE | AWS::Lambda::Function | testFunction (testFunction483F4CBA) 4/4 | 0:02:34 | CREATE_COMPLETE | AWS::CloudFormation::Stack | LambdaUnitStack ✅ LambdaUnitStack Stack ARN: arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/LambdaUnitStack/99999999-b3d0-11ea-ad3a-999999999999 ✨ Done in 73.86s.
上記のような表示がでればデプロイは問題なく完了です。念のためAWSのWebコンソールで状態を確認します。先ほどトランスパイルしたJavaScriptのコードがweb上でも確認できます。
念のためデフォルトでテストイベントを作成してテストしたところ問題なく動作することが確認できました。
感想
webpack使うだけの記事がなさそうだったので紹介しました。単発で書き捨てる開発なら単純にトランスパイルでも良いですが、ライブラリの更新など継続的な開発を意識した場合はこのような設定をとって開発が行われています。
自分も普段は出来合いの設定を使っていたので、最小限必要な設定が分かり勉強になりました。他にも色々方法はありますが結構使いやすいかと思いますのでよかったら試してみてください。